// @ts-check

/**
 * Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options
 */

import * as fs from 'fs';
import { builtinModules } from 'module';
import * as path from 'path';
import { fileURLToPath } from 'url';

import deepMerge from 'deepmerge';

import { defineConfig } from 'rollup';
import {
  makeCleanupPlugin,
  makeDebugBuildStatementReplacePlugin,
  makeNodeResolvePlugin,
  makeProductionReplacePlugin,
  makeRrwebBuildPlugin,
  makeSucrasePlugin,
} from './plugins/index.mjs';
import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs';
import { mergePlugins } from './utils.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }));

const ignoreSideEffects = /[\\\/]debug-build\.ts$/;

export function makeBaseNPMConfig(options = {}) {
  const {
    entrypoints = ['src/index.ts'],
    hasBundles = false,
    packageSpecificConfig = {},
    sucrase = {},
    bundledBuiltins = [],
  } = options;

  const nodeResolvePlugin = makeNodeResolvePlugin();
  const sucrasePlugin = makeSucrasePlugin({}, sucrase);
  const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin();
  const cleanupPlugin = makeCleanupPlugin();
  const rrwebBuildPlugin = makeRrwebBuildPlugin({
    excludeShadowDom: undefined,
    excludeIframe: undefined,
  });

  const defaultBaseConfig = {
    input: entrypoints,

    output: {
      // an appropriately-named directory will be added to this base value when we specify either a cjs or esm build
      dir: hasBundles ? 'build/npm' : 'build',

      sourcemap: true,

      // Include __esModule property when there is a default prop
      esModule: 'if-default-prop',

      // output individual files rather than one big bundle
      preserveModules: true,

      // Allow wrappers or helper functions generated by rollup to use any ES2015 features
      generatedCode: {
        preset: 'es2015',
      },

      // don't add `"use strict"` to the top of cjs files
      strict: false,

      // do TS-3.8-style exports
      //     exports.dogs = are.great
      // rather than TS-3.9-style exports
      //     Object.defineProperty(exports, 'dogs', {
      //       enumerable: true,
      //       get: () => are.great,
      //     });
      externalLiveBindings: false,

      // Don't call `Object.freeze` on the results of `import * as someModule from '...'`
      // (We don't need it, so why waste the bytes?)
      freeze: false,

      interop: 'esModule',
    },

    treeshake: {
      moduleSideEffects: (id, external) => {
        if (external === false && ignoreSideEffects.test(id)) {
          // Tell Rollup this module has no side effects, so it can be tree-shaken
          return false;
        }

        return true;
      },
    },

    plugins: [nodeResolvePlugin, sucrasePlugin, debugBuildStatementReplacePlugin, rrwebBuildPlugin, cleanupPlugin],

    // don't include imported modules from outside the package in the final output
    external: [
      ...builtinModules.filter(m => !bundledBuiltins.includes(m)),
      ...Object.keys(packageDotJSON.dependencies || {}),
      ...Object.keys(packageDotJSON.peerDependencies || {}),
      ...Object.keys(packageDotJSON.optionalDependencies || {}),
    ],
  };

  return deepMerge(defaultBaseConfig, packageSpecificConfig, {
    // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them
    customMerge: key => (key === 'plugins' ? mergePlugins : undefined),
  });
}

export function makeNPMConfigVariants(baseConfig, options = {}) {
  const { emitEsm = true, emitCjs = true, splitDevProd = false } = options;

  const variantSpecificConfigs = [];

  if (emitCjs) {
    if (splitDevProd) {
      variantSpecificConfigs.push({ output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs/dev') } });
      variantSpecificConfigs.push({
        output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs/prod') },
        plugins: [makeProductionReplacePlugin()],
      });
    } else {
      variantSpecificConfigs.push({ output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } });
    }
  }

  if (emitEsm) {
    if (splitDevProd) {
      variantSpecificConfigs.push({
        output: {
          format: 'esm',
          dir: path.join(baseConfig.output.dir, 'esm/dev'),
          plugins: [makePackageNodeEsm()],
        },
      });
      variantSpecificConfigs.push({
        output: {
          format: 'esm',
          dir: path.join(baseConfig.output.dir, 'esm/prod'),
          plugins: [makeProductionReplacePlugin(), makePackageNodeEsm()],
        },
      });
    } else {
      variantSpecificConfigs.push({
        output: {
          format: 'esm',
          dir: path.join(baseConfig.output.dir, 'esm'),
          plugins: [makePackageNodeEsm()],
        },
      });
    }
  }

  return variantSpecificConfigs.map(variant => deepMerge(baseConfig, variant));
}

/**
 * This creates a loader file at the target location as part of the rollup build.
 * This loader script can then be used in combination with various Node.js flags (like --import=...) to monkeypatch 3rd party modules.
 */
export function makeOtelLoaders(outputFolder, hookVariant) {
  if (hookVariant !== 'otel' && hookVariant !== 'sentry-node') {
    throw new Error('hookVariant is neither "otel" nor "sentry-node". Pick one.');
  }

  const expectedRegisterLoaderLocation = `${outputFolder}/import-hook.mjs`;
  const foundRegisterLoaderExport = Object.keys(packageDotJSON.exports ?? {}).some(key => {
    return packageDotJSON?.exports?.[key]?.import?.default === expectedRegisterLoaderLocation;
  });
  if (!foundRegisterLoaderExport) {
    throw new Error(
      `You used the makeOtelLoaders() rollup utility without specifying the import hook inside \`exports[something].import.default\`. Please add "${expectedRegisterLoaderLocation}" as a value there (maybe check for typos - it needs to be "${expectedRegisterLoaderLocation}" exactly).`,
    );
  }

  const expectedHooksLoaderLocation = `${outputFolder}/loader-hook.mjs`;
  const foundHookLoaderExport = Object.keys(packageDotJSON.exports ?? {}).some(key => {
    return packageDotJSON?.exports?.[key]?.import?.default === expectedHooksLoaderLocation;
  });
  if (!foundHookLoaderExport) {
    throw new Error(
      `You used the makeOtelLoaders() rollup utility without specifying the loader hook inside \`exports[something].import.default\`. Please add "${expectedHooksLoaderLocation}" as a value there (maybe check for typos - it needs to be "${expectedHooksLoaderLocation}" exactly).`,
    );
  }

  const requiredDep = hookVariant === 'otel' ? '@opentelemetry/instrumentation' : '@sentry/node';
  const foundImportInTheMiddleDep =
    Object.keys(packageDotJSON.dependencies ?? {}).some(key => {
      return key === requiredDep;
    }) ||
    Object.keys(packageDotJSON.devDependencies ?? {}).some(key => {
      return key === requiredDep;
    });

  if (!foundImportInTheMiddleDep) {
    throw new Error(
      `You used the makeOtelLoaders() rollup utility but didn't specify the "${requiredDep}" dependency in ${path.resolve(
        process.cwd(),
        'package.json',
      )}. Please add it to the dependencies.`,
    );
  }

  return defineConfig([
    // register() hook
    {
      input: path.join(
        __dirname,
        'code',
        hookVariant === 'otel' ? 'otelEsmImportHookTemplate.js' : 'sentryNodeEsmImportHookTemplate.js',
      ),
      external: /.*/,
      output: {
        format: 'esm',
        file: path.join(outputFolder, 'import-hook.mjs'),
      },
    },
    // --loader hook
    {
      input: path.join(
        __dirname,
        'code',
        hookVariant === 'otel' ? 'otelEsmLoaderHookTemplate.js' : 'sentryNodeEsmLoaderHookTemplate.js',
      ),
      external: /.*/,
      output: {
        format: 'esm',
        file: path.join(outputFolder, 'loader-hook.mjs'),
      },
    },
  ]);
}
